To begin this chapter, let's examine the details of defining methods. Just like the Main() method (see Chapter 3), your custom methods may or may not take parameters and may or may not return values to the caller. As you will see over the next several chapters, methods can be implemented within the scope of classes or structures (as well as prototyped within interface types) and may be decorated with various keywords (e.g., internal, virtual, public, new) to qualify their behavior. At this point in the text, each of our methods has followed this basic format:
// Recall that static methods can be called directly // without creating a class instance. class Program { // static returnType MethodName(params) {...} static int Add(int x, int y){ return x + y; } }
While the definition of a method in C# is quite straightforward, there are a handful of keywords that you can use to control how arguments are passed to the method in question. These are listed in Table 4-1.
Parameter Modifier | Meaning in Life |
---|---|
(None) | If a parameter is not marked with a parameter modifier, it is assumed to be passed by value, meaning the called method receives a copy of the original data. |
out | Output parameters must be assigned by the method being called, and therefore are passed by reference. If the called method fails to assign output parameters, you are issued a compiler error. |
ref | The value is initially assigned by the caller and may be optionally reassigned by the called method (as the data is also passed by reference). No compiler error is generated if the called method fails to assign a ref parameter. |
params | This parameter modifier allows you to send in a variable number of arguments as a single logical parameter. A method can have only a single params modifier, and it must be the final parameter of the method. In reality, you may not need to use the params modifier all too often, however be aware that numerous methods within the base class libraries do make use of this C# language feature. |
To illustrate the use of these keywords, create a new Console Application project named FunWithMethods. Now, let's walk through the role of each keyword.
The default manner in which a parameter is sent into a function is by value. Simply put, if you do not mark an argument with a parameter modifier, a copy of the data is passed into the function. As explained at the end of this chapter, exactly what is copied will depend on whether the parameter is a value type or a reference type. For the time being, assume the following method within the Program class that operates on two numerical data types passed by value:
// Arguments are passed by value by default. static int Add(int x, int y) { int ans = x + y; // Caller will not see these changes // as you are modifying a copy of the // original data. x = 10000; y = 88888; return ans; }
Numerical data falls under the category of value types. Therefore, if you change the values of the parameters within the scope of the member, the caller is blissfully unaware, given that you are changing the values on a copy of the caller's original data:
static void Main(string[] args) { Console.WriteLine("***** Fun with Methods *****\n"); // Pass two variables in by value. int x = 9, y = 10; Console.WriteLine("Before call: X: {0}, Y: {1}", x, y); Console.WriteLine("Answer is: {0}", Add(x, y)); Console.WriteLine("After call: X: {0}, Y: {1}", x, y); Console.ReadLine(); }
As you would hope, the values of x and y remain identical before and after the call to Add(), as shown in the following:
***** Fun with Methods *****
Before call: X: 9, Y: 10
Answer is: 19
After call: X: 9, Y: 10
Next, you have the use of output parameters. Methods that have been defined to take output parameters (via the out keyword) are under obligation to assign them to an appropriate value before exiting the method scope (if you fail to do so, you will receive compiler errors).
To illustrate, here is an alternative version of the Add() method that returns the sum of two integers using the C# out modifier (note the physical return value of this method is now void):
// Output parameters must be assigned by the called method. static void Add(int x, int y, out int ans) { ans = x + y; }
Calling a method with output parameters also requires the use of the out modifier. However, the local variables which are passed as output variables are not required to be assigned before passing them in as output arguments (if you do so, the original value is lost after the call). The reason the compiler allows you to send in seemingly unassigned data is due to the fact that the method being called must make an assignment. The following code is an example:
static void Main(string[] args) { Console.WriteLine("***** Fun with Methods *****"); ... // No need to assign initial value to local variables // used as output parameters, provided the first time // you use them is as output arguments. int ans; Add(90, 90, out ans); Console.WriteLine("90 + 90 = {0}", ans); Console.ReadLine(); }
The previous example is intended to be illustrative in nature; you really have no reason to return the value of your summation using an output parameter. However, the C# out modifier does serve a very useful purpose: it allows the caller to obtain multiple return values from a single method invocation.
// Returning multiple output parameters. static void FillTheseValues(out int a, out string b, out bool c) { a = 9; b = "Enjoy your string."; c = true; }
The caller would be able to invoke the FillTheseValues() method. Do notice that you must use the out modifier when you invoke the method, as well as when you implement the method:
static void Main(string[] args) { Console.WriteLine("***** Fun with Methods *****"); ... int i; string str; bool b; FillTheseValues(out i, out str, out b); Console.WriteLine("Int is: {0}", i); Console.WriteLine("String is: {0}", str); Console.WriteLine("Boolean is: {0}", b); Console.ReadLine(); }
Finally, always remember that a method that defines output parameters must assign the parameter to a valid value before exiting the method scope. Therefore, the following code will result in a compiler error, as the output parameter has not been assigned within the method scope:
static void ThisWontCompile(out int a) { Console.WriteLine("Error! Forgot to assign output arg!"); }
Now consider the use of the C# ref parameter modifier. Reference parameters are necessary when you wish to allow a method to operate on (and usually change the values of) various data points declared in the caller's scope (such as a sorting or swapping routine). Note the distinction between output and reference parameters:
Let's check out the use of the ref keyword by way of a method that swaps two string variables:
// Reference parameters. public static void SwapStrings(ref string s1, ref string s2) { string tempStr = s1; s1 = s2; s2 = tempStr; }
This method can be called as follows:
static void Main(string[] args) { Console.WriteLine("***** Fun with Methods *****"); ... string s1 = "Flip"; string s2 = "Flop"; Console.WriteLine("Before: {0}, {1} ", s1, s2); SwapStrings(ref s1, ref s2); Console.WriteLine("After: {0}, {1} ", s1, s2); Console.ReadLine(); }
Here, the caller has assigned an initial value to local string data (s1 and s2). Once the call to SwapStrings() returns, s1 now contains the value "Flop", while s2 reports the value "Flip":
Before: Flip, Flop After: Flop, Flip
Note The C# ref keyword will be revisited later in this chapter in the section "Understanding Value Types and Reference Types." As you will see, the behavior of this keyword changes just a bit depending on whether the argument is a value type (structure) or reference type (class).
C# supports the use of parameter arrays using the params keyword. To understand this language feature you must (as the name implies) understand how to manipulate C# arrays. If this is not the case, you may wish to return to this section once you read the section "Array Manipulation in C#" found a bit later in this chapter.
The params keyword allows you to pass into a method a variable number of identically typed parameters as a single logical parameter. As well, arguments marked with the params keyword can be processed if the caller sends in a strongly typed array or a comma-delimited list of items. Yes, this can be confusing! To clear things up, assume you wish to create a function that allows the caller to pass in any number of arguments and return the calculated average.
If you were to prototype this method to take an array of doubles, this would force the caller to first define the array, then fill the array, and finally pass it into the method. However, if you define CalculateAverage() to take a params of double data types, the caller can simply pass a comma-delimited list of doubles. The .NET runtime will automatically package the set of doubles into an array of type double behind the scenes:
// Return average of "some number" of doubles. static double CalculateAverage(params double[] values) { Console.WriteLine("You sent me {0} doubles.", values.Length); double sum = 0; if(values.Length == 0) return sum; for (int i = 0; i < values.Length; i++) sum += values[i]; return (sum / values.Length); }
This method has been defined to take a parameter array of doubles. What this method is in fact saying is, "Send me any number of doubles (including zero) and I'll compute the average." Given this, you can call CalculateAverage() in any of the following ways:
static void Main(string[] args) { Console.WriteLine("***** Fun with Methods *****"); ... // Pass in a comma-delimited list of doubles... double average; average = CalculateAverage(4.0, 3.2, 5.7, 64.22, 87.2); Console.WriteLine("Average of data is: {0}", average); // ...or pass an array of doubles. double[] data = { 4.0, 3.2, 5.7 }; average = CalculateAverage(data); Console.WriteLine("Average of data is: {0}", average); // Average of 0 is 0! Console.WriteLine("Average of data is: {0}", CalculateAverage()); Console.ReadLine(); }
If you did not make use of the params modifier in the definition of CalculateAverage(), the first invocation of this method would result in a compiler error, as the compiler would be looking for a version of CalculateAverage() which took five double arguments.
Note To avoid any ambiguity, C# demands a method only support single params argument, which must be the final argument in the parameter list.
As you might guess, this technique is nothing more than a convenience for the caller, given that the array is created by the CLR as necessary. By the time the array is within the scope of the method being called, you are able to treat it as a full-blown .NET array that contains all the functionality of the System.Array base class library type. Consider the following output:
You sent me 5 doubles. Average of data is: 32.864 You sent me 3 doubles. Average of data is: 4.3 You sent me 0 doubles. Average of data is: 0
With the release of .NET 4.0, C# programmers are now able to create methods which can take optional arguments. This technique allows the caller to invoke a single method while omitting arguments deemed unnecessary, provided the caller is happy with the specified defaults.
Note As you will see in Chapter 18, a key motivation for adding optional arguments to C# is to simplify interacting with COM objects. Several Microsoft object models (e.g., Microsoft Office) expose their functionality via COM objects, many of which were written long ago to make use of optional parameters.
To illustrate working with optional arguments, assume you have a method named EnterLogData(), which defines a single optional parameter:
static void EnterLogData(string message, string owner = "Programmer") { Console.Beep(); Console.WriteLine("Error: {0}", message); Console.WriteLine("Owner of Error: {0}", owner); }
Here, the final string argument has been assigned the default value of "Programmer", via an assignment within the parameter definition. Given this, we can call EnterLogData() from within Main() in two manners:
static void Main(string[] args) { Console.WriteLine("***** Fun with Methods *****"); ... EnterLogData("Oh no! Grid can't find data"); EnterLogData("Oh no! I can't find the payroll data", "CFO"); Console.ReadLine(); }
Because the first invocation of EnterLogData() did not specify a second string argument, we would find that the programmer is the one responsible for losing data for the grid, while the CFO misplaced the payroll data (as specified by the second argument in the second method call).
One very important thing to be aware of, is that the value assigned to an optional parameter must be known at compile time, and cannot be resolved at runtime (if you attempt to do so, you'll receive compile time errors!). To illustrate, assume you wish to update EnterLogData() with the following extra optional parameter:
// Error! The default value for an optional arg must be known // at compile time! static void EnterLogData(string message, string owner = "Programmer", DateTime timeStamp = DateTime.Now) { Console.Beep(); Console.WriteLine("Error: {0}", message); Console.WriteLine("Owner of Error: {0}", owner); Console.WriteLine("Time of Error: {0}", timeStamp); }
This will not compile, as the value of the Now property of the DateTime class is resolved at runtime, not compile time.
Note To avoid ambiguity, optional parameters must always be packed onto the end of a method signature. It is a compiler error to have optional parameters listed before non-optional parameters.
Another new language feature added to C# with the release of .NET 4.0 is support for named arguments. To be honest, at first glance, this language construct may appear to do little more than result in confusing code. And to continue being completely honest, this could be the case! Much like optional arguments, including support for named parameters are largely motivated by the desire to simplify the process of working with the COM interoperability layer.
Named arguments allow you to invoke a method by specifying parameter values in any order you choose. Thus, rather than passing parameters solely by position (as you will do in most cases), you can choose to specify each argument by name using a colon operator. To illustrate the use of named arguments, assume we have added the following method to the Program class:
static void DisplayFancyMessage(ConsoleColor textColor, ConsoleColor backgroundColor, string message) { // Store old colors to restore once message is printed. ConsoleColor oldTextColor = Console.ForegroundColor; ConsoleColor oldbackgroundColor = Console.BackgroundColor; // Set new colors and print message. Console.ForegroundColor = textColor; Console.BackgroundColor = backgroundColor; Console.WriteLine(message); // Restore previous colors. Console.ForegroundColor = oldTextColor; Console.BackgroundColor = oldbackgroundColor; }
Now, the way DisplayFancyMessage() was written, you would expect the caller to invoke this method by passing two ConsoleColor variables followed by a string type. However, using named arguments, the following calls are completely fine:
static void Main(string[] args) { Console.WriteLine("***** Fun with Methods *****"); ... DisplayFancyMessage(message: "Wow! Very Fancy indeed!", textColor: ConsoleColor.DarkRed, backgroundColor: ConsoleColor.White); DisplayFancyMessage(backgroundColor: ConsoleColor.Green, message: "Testing...", textColor: ConsoleColor.DarkBlue); Console.ReadLine(); }
One minor gotcha regarding named arguments is that if you begin to invoke a method using positional parameters, they must be listed before any named parameters. In other words, named arguments must always be packed onto the end of a method call. The following code is an example:
// This is OK, as positional args are listed before named args. DisplayFancyMessage(ConsoleColor.Blue, message: "Testing...", backgroundColor: ConsoleColor.White); // This is an ERROR, as positional args are listed after named args. DisplayFancyMessage(message: "Testing...", backgroundColor: ConsoleColor.White, ConsoleColor.Blue);
This restriction aside, you might still be wondering when you would ever want to use this language feature. After all, if you need to specify three arguments to a method, why bother flipping around their position?
Well, as it turns out, if you have a method that defines optional arguments, this feature can actually be really helpful. Assume DisplayFancyMessage() has been rewritten to now support optional arguments, as you have assigned fitting defaults:
static void DisplayFancyMessage(ConsoleColor textColor = ConsoleColor.Blue, ConsoleColor backgroundColor = ConsoleColor.White, string message = "Test Message") { ... }
Given that each argument has a default value, named arguments allows the caller to only specify the parameter(s) for which they do not wish to receive the defaults. Therefore, if the caller wants the value "Hello!" to appear in blue text surrounded by a white background, they can simply say:
DisplayFancyMessage(message: "Hello!");
Or, if the caller wants to see "Test Message" print out with a green background containing blue text, they can invoke DisplayFancyMessage():
DisplayFancyMessage(backgroundColor: ConsoleColor.Green);
As you can see, optional arguments and named parameters do tend to work hand in hand. To wrap up our examination of building C# methods, I need to address the topic of method overloading.
Like other modern object-oriented languages, C# allows a method to be overloaded. Simply put, when you define a set of identically named method that differ by the number (or type) of parameters, the method in question is said to be overloaded.
To understand why overloading is so useful, consider life as a Visual Basic 6.0 developer. Assume you are using VB6 to build a set of methods that return the sum of various incoming data types (Integers, Doubles, and so on). Given that VB6 does not support method overloading, you would be required to define a unique set of methods that essentially do the same thing (return the sum of the arguments):
' VB6 code examples. Public Function AddInts(ByVal x As Integer, ByVal y As Integer) As Integer AddInts = x + y End Function Public Function AddDoubles(ByVal x As Double, ByVal y As Double) As Double AddDoubles = x + y End Function Public Function AddLongs(ByVal x As Long, ByVal y As Long) As Long AddLongs = x + y End Function
Not only can code such as this become tough to maintain, but the caller must now be painfully aware of the name of each method. Using overloading, you are able to allow the caller to call a single method named Add(). Again, the key is to ensure that each version of the method has a distinct set of arguments (methods differing only by return type are not unique enough).
Note As explained in Chapter 10, it is possible to build generic methods that take the concept of overloading to the next level. Using generics, you can define "placeholders" for a method implementation that are specified at the time you invoke the member.
To check this out firsthand, create a new Console Application project named MethodOverloading. Now, consider the following class definition:
// C# code. class Program { static void Main(string[] args) { } // Overloaded Add() method. static int Add(int x, int y) { return x + y; } static double Add(double x, double y) { return x + y; } static long Add(long x, long y) { return x + y; } }
The caller can now simply invoke Add() with the required arguments and the compiler is happy to comply, given the fact that the compiler is able to resolve the correct implementation to invoke given the provided arguments:
static void Main(string[] args) { Console.WriteLine("***** Fun with Method Overloading *****\n"); // Calls int version of Add() Console.WriteLine(Add(10, 10)); // Calls long version of Add() Console.WriteLine(Add(900000000000, 900000000000)); // Calls double version of Add() Console.WriteLine(Add(4.3, 4.4)); Console.ReadLine(); }
The Visual Studio 2010 IDE provides assistance when calling overloaded methods to boot. When you type in the name of an overloaded method (such as our good friend Console.WriteLine()), IntelliSense will list each version of the method in question. Note that you are able to cycle through each version of an overloaded method using the up and down arrow keys shown in Figure 4-1.
Figure 4-1. Visual Studio IntelliSense for overloaded methods
Source Code The MethodOverloading application is located under the Chapter 4 subdirectory.
That wraps up our initial examination of building methods using the syntax of C#. Next, let's check out how to build and manipulate arrays, enumerations, and structures. After this point, we will wrap up the chapter with an examination of 'nullable data types' and the C# ? and ?? operators.